﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Collections;
using System.Linq;
using System.Text.RegularExpressions;

namespace DynamiteXml.HelperLogic
{
    internal static class XmlSafeTextLogic
    {
        internal static void EnforceXmlSafeText(object obj)
        {
            evaluateAllStringProperties(obj, serializingMode.Serialize);
        }

        internal static void RevertEnforceXmlSafeText(object obj)
        {
            evaluateAllStringProperties(obj, serializingMode.Deserialize);
        }

        private enum serializingMode
        {
            Serialize,
            Deserialize,
        }

        private const char startEscapeCharacter = '\\';
        private const char endEscapeCharacter = ';';
        private static string composeEscapeString(char value)
        {
            return startEscapeCharacter + ((int)value).ToString("x2") + endEscapeCharacter;
        }
        
        private static char deComposeEscapeString(string value)
        {
            return (char)Convert.ToInt32(value.Substring(1, value.Length - 2));
        }

        private static bool characterShouldBeEscaped(char c)
        {
            // Characters that should be escaped are based on the list found at:
            // http://www.w3.org/TR/xml11/#charsets
            uint charvalue = (uint)c;
            if (charvalue <= 255)
            {
                if (charvalue <= 0x8)
                    return true;
                else if ((charvalue >= 0xB) && (charvalue <= 0x1F) && (charvalue != 0xD))
                {
                    // Ranges [#xB-#xC] and [#xE-#x1F].
                    // Which effectively means: [#xB-#x1F] excluding #xD.
                    return true;
                }
                else if ((charvalue >= 0x7F) && (charvalue <= 0x9F) && (charvalue != 0x85))
                {
                    // Ranges [#x7F-#x84] and [#x86-#x9F].
                    // Which effectively means: [#x7F-#x9F] excluding #x85.
                    return true;
                }
                else
                    return false;
            }
            else
            {
                #region Optimization: put characters > #xFF in else branch
                //Chance that these ranges need to be evaluated is low.
                if ((charvalue >= 0xFDD0) && (charvalue <= 0xFDDF))
                    return true;
                if ((charvalue >= 0x1FFFE) && (charvalue <= 0x1FFFF))
                    return true;
                if ((charvalue >= 0x2FFFE) && (charvalue <= 0x2FFFE))
                    return true;
                if ((charvalue >= 0x3FFFE) && (charvalue <= 0x3FFFF))
                    return true;
                if ((charvalue >= 0x4FFFE) && (charvalue <= 0x4FFFF))
                    return true;
                if ((charvalue >= 0x5FFFE) && (charvalue <= 0x5FFFF))
                    return true;
                if ((charvalue >= 0x6FFFE) && (charvalue <= 0x6FFFF))
                    return true;
                if ((charvalue >= 0x7FFFE) && (charvalue <= 0x7FFFF))
                    return true;
                if ((charvalue >= 0x8FFFE) && (charvalue <= 0x8FFFF))
                    return true;
                if ((charvalue >= 0x9FFFE) && (charvalue <= 0x9FFFF))
                    return true;
                if ((charvalue >= 0xAFFFE) && (charvalue <= 0xAFFFF))
                    return true;
                if ((charvalue >= 0xBFFFE) && (charvalue <= 0xBFFFF))
                    return true;
                if ((charvalue >= 0xCFFFE) && (charvalue <= 0xCFFFF))
                    return true;
                if ((charvalue >= 0xDFFFE) && (charvalue <= 0xDFFFF))
                    return true;
                if ((charvalue >= 0xEFFFE) && (charvalue <= 0xEFFFF))
                    return true;
                if ((charvalue >= 0xFFFFE) && (charvalue <= 0xFFFFF))
                    return true;
                if ((charvalue >= 0x10FFFE) && (charvalue <= 0x10FFFF))
                    return true;
                else 
                    return false;
                #endregion
            }
        }

        private static string EscapeCharacters(string value)
        {
            #region Comment: Thoughts about the approach to escape characters.
            // -> Replace all charaters that should be replaced.
            // Not wise to loop over every possible charater eligible for escaping.
            // Too much overhead.
            // -> go over each charater in the string & replace if it needs to be replaced.
            // Better, but if the same charater appears more than once, 
            // the replace method is called too often.
            // -> go over each charater in the string & buffer if it needs to be replaced.
            // Invoke the replace method for each distinct character in the list.
            #endregion
            
            List<char> charactersToBeReplaced = new List<char>();            
            for (int i = 0; i < value.Length; i++)
            {
                if (characterShouldBeEscaped(value[i]))
                    charactersToBeReplaced.Add(value[i]);
            }
            charactersToBeReplaced = new List<char>(charactersToBeReplaced.Distinct());

            StringBuilder sb = new StringBuilder(value);

            // First escape startEscapeCharacter (endEscapeCharacter is not required)
            sb = sb.Replace(startEscapeCharacter.ToString(), composeEscapeString(startEscapeCharacter));

            // Then escape characters listed in the dictionary
            foreach (char characterToBeReplaced in charactersToBeReplaced)
            {
                sb = sb.Replace(characterToBeReplaced.ToString(), composeEscapeString(characterToBeReplaced));
            }

            return sb.ToString();
        }

        private static string RevertEscapedCharacters(string value)
        {
            Regex r = new Regex(Regex.Escape(startEscapeCharacter.ToString()) + "[a-fA-F\\d]*" + Regex.Escape(endEscapeCharacter.ToString()));
            MatchCollection matchCollection = r.Matches(value);

            List<string> matches = new List<string>();
            foreach (Match match in matchCollection)
            {
                if (match.Value != composeEscapeString(startEscapeCharacter))
                    matches.Add(match.Value);
            }
            matches = new List<string>(matches.Distinct());

            // First reverse escaped characters listed in the dictionary
            StringBuilder sb = new StringBuilder(value);
            foreach (string match in matches)
            {
                // The startEscapeCharacter match should be reverted as last.
                    sb.Replace(match, deComposeEscapeString(match).ToString());
            }
            
            // Then reverse the startEscapeCharacter
            sb.Replace(composeEscapeString(startEscapeCharacter), startEscapeCharacter.ToString());

            return sb.ToString();
        }

        private static void evaluateAllStringProperties(object obj, serializingMode serializingMode)
        {
            if (obj != null)
            {
                foreach (PropertyInfo pi in obj.GetType().GetProperties())
                {
                    if (!(pi.PropertyType == typeof(Serializer)) &&
                        !(pi.PropertyType == typeof(TypeIdentifier)) &&
                        !(pi.PropertyType == typeof(StringBuilder)) && pi.GetGetMethod() != null)
                    {
                        object value = pi.GetValue(obj, null);
                        if ((value.GetType().IsValueType) || (value is decimal))
                        {
                            // Nothing to do, no need to inspect properties.
                        }
                        else if (value is string)
                        {
                            if (pi.GetSetMethod() != null)
                            {
                                string newValue;
                                switch (serializingMode)
                                {
                                    case serializingMode.Serialize:
                                        newValue = EscapeCharacters((string)value);
                                        break;
                                    case serializingMode.Deserialize:
                                        newValue = RevertEscapedCharacters((string)value);
                                        break;
                                    default:
                                        throw new Exception(string.Format("Invalid serializingMode: {0}", serializingMode));
                                }

                                pi.SetValue(obj, newValue, null);
                            }
                        }
                        else if (value is IList)
                        {
                            foreach (object listItem in (IList)value)
                            {
                                evaluateAllStringProperties(listItem, serializingMode);
                            }
                        }
                        else
                        {
                            evaluateAllStringProperties(value, serializingMode);
                        }
                    }
                }
            }
        }
    }
}
